import {
  Vector3,
  Color,
  Texture,
  WebGLRenderer,
  PerspectiveCamera,
  Group,
  Scene,
  DirectionalLight,
  AmbientLight,
  PlaneGeometry,
  Mesh,
  PCFSoftShadowMap,
  Fog,
  AxesHelper,
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { presetOtherMaterials } from '../planClass/model/materials'
import { generate } from '../planClass/model/generate'
import { disposeAll } from '../../utils/disposeAll'
import { _COLOR } from '../color'

import type { MagizTypes } from '../../types/magizTypes'

export { View }

// notes:
// js 必须在的一定数值范围内显示，否则会出现异常。这是3D渲染通病。如按
// 经纬度计算数值过小，出现部分EdgesGeometry不显示的问题。如离原点过远，会出
// 现模型旋转时不停闪烁的问题，即使设置logarithmicDepthBuffer 为 true，也
// 无法完全解决。
//
// 提升效率的方法：render.shadowMap.autoUpdate 关闭阴影自动更新
//
// 从instancedMesh到Mesh用 SceneUtils.createMeshesFromInstancedMesh
// https://threejs.org/docs/#examples/zh/utils/SceneUtils

const viewOptions: MagizTypes.ViewOptions = {
  fog: { near: 900, far: 5000 },
  groundSize: 5000,
  cameraPosition: { x: 200, y: 10, z: 200 },
  sunDistance: 10000,
  lightColor: [
    { hour: 5, color: '#116', directional: 0, ambient: 0 },
    { hour: 6, color: '#f60', directional: 0.6, ambient: 0.2 },
    { hour: 9, color: '#eee', directional: 2, ambient: 0.6 },
    { hour: 12, color: '#eee', directional: 2, ambient: 0.6 },
    { hour: 16, color: '#eee', directional: 2, ambient: 0.6 },
    { hour: 18, color: '#d33', directional: 0.6, ambient: 0.2 },
    { hour: 19, color: '#116', directional: 0.1, ambient: 0.1 },
    { hour: 24, color: '#000', directional: 0, ambient: 0 },
  ],
  time: 10,
  shadow: true,
}

const orbitControlsOptions = {
  /** 速度大于 0 会自动旋转镜头 */
  autoRotate: 0,
  /** 缩放距离小于 moveNear 会持续向前移动 */
  moveNear: 0,
  zoomToCursor: false,
  enableDamping: true,
  enablePan: true,
}

/** 管理Three.js场景及相关内容的工具类 */
class View {
  /** 创建的 Three.js 渲染器实例 */
  renderer: WebGLRenderer
  /** 创建的 Three.js 镜头实例 */
  camera: PerspectiveCamera
  /** 创建的 Three.js 控制器实例 */
  controls: OrbitControls
  /** 创建的 Three.js 场景 */
  scene: Scene
  /** 场景中不被自动清理的的元素（如光、地面、Helpers） */
  ignored: Group
  /** 场景中的直射光和环境光 */
  lights: {
    directional: DirectionalLight
    ambient: AmbientLight
  }
  /** 场景中的动画函数 */
  animations: { [name: string]: () => void }
  /** 太阳坐标 */
  sunPosition: Vector3
  /** 工具实例的参数 */
  options: MagizTypes.ViewOptions
  /** 绑定DOM元素，并生成用于Three.js渲染场景的canvas元素 */
  parent: HTMLDivElement
  /** 独立缓存重映射相关数据用于还原 */
  remapCaching: {
    envMapTexture: Texture | null
    edge: string
    ground: string
    lightFogSky: string
  }
  /** 场景中用到的颜色 */
  colors: { [name: string]: Color }

  /** 创建管理工具实例 */
  constructor(
    /** 通过querySelector绑定Canvas到Div */
    divID: string,
    /** 初始化工具实例的参数 */
    options?: Partial<MagizTypes.ViewOptions>
  ) {
    const div = document.getElementById(divID) as HTMLDivElement
    if (!div) throw 'ERROR: invalid divID'
    const canvas = document.createElement('canvas')
    div.appendChild(canvas)
    this.parent = div
    this.colors = {}
    this.scene = new Scene()
    this.ignored = new Group()
    this.animations = {}
    this.options = Object.assign(viewOptions, options)
    this.renderer = new WebGLRenderer({
      // logarithmicDepthBuffer: true,
      preserveDrawingBuffer: true,
      antialias: true,
      canvas,
    })
    this.renderer.setPixelRatio(window.devicePixelRatio)

    // 重映射相关设置
    this.remapCaching = {
      envMapTexture: null,
      edge: _COLOR.EDGE,
      ground: _COLOR.GROUND,
      lightFogSky: _COLOR.SKY,
    }

    // 阴影设置案例 https://threejs.org/docs/index.html?q=DirectionalLight#api/en/lights/shadows/DirectionalLightShadow
    // 部分材质不会产生阴影 https://threejs.org/manual/#zh/materials
    // 阴影效果可通过 light.shadow.mapSize 和 renderer.shadowMap.type 进行调整 https://threejs.org/docs/index.html#api/zh/constants/Renderer
    const ambient = new AmbientLight('#fff', 0)
    const directional = new DirectionalLight()
    this.lights = { directional, ambient }

    // 解决z-fighting的可用参数:
    // Material.polygonOffset + Material.polygonOffsetFactor + Material.polygonOffsetUnits
    // Material.depthWrite
    // 关闭 renderer.logarithmicDepthBuffer
    // Mesh.renderOrder

    // https://threejs.org/docs/#api/zh/lights/DirectionalLight.target
    this.ignored.add(ambient, directional, directional.target)
    this.ignored.userData.ignored = true
    this.scene.add(this.ignored)

    if (this.options.shadow) {
      directional.shadow.mapSize.set(4096, 4096)
      directional.castShadow = true
      const sc = directional.shadow.camera
      sc.far = this.options.sunDistance * 2

      const { shadowMap } = this.renderer
      shadowMap.type = PCFSoftShadowMap
      shadowMap.enabled = true

      this.setShadowArea(200, 200)
    }

    this.camera = new PerspectiveCamera(45, 1, 1, 1000000000)
    this.camera.position.copy(this.options.cameraPosition)
    this.controls = initOrbitControls(this)

    // 先初始化太阳位置才能设置时间
    this.sunPosition = new Vector3()
    this.setTime(this.options.time)

    window.addEventListener('resize', () => this.resizeScene())
    this.resizeScene()

    /////////////////// ANIMATIONS ///////////////////
    // 按需渲染会增加很多代码合复杂性！
    animate(this)
  }

  setShadowArea(width: number, height: number) {
    const { camera } = this.lights.directional.shadow
    camera.bottom = -(camera.top = height)
    camera.left = -(camera.right = width)
    camera.updateProjectionMatrix()
  }

  setShadow(on: boolean) {
    this.lights.directional.castShadow = on
  }

  /** 按24小时划分一天，设置场景的光影 */
  setTime(hour: number) {
    hour = hour % 24
    const { directional: dl, ambient: al } = this.lights
    let ambientIntensity: number
    let directionalIntensity: number
    let color: Color

    const { lightColor } = this.options
    lightColor.find((lc, i) => {
      if (lc.hour > hour) {
        // 计算颜色与强度
        const pc = lightColor[(i === 0 ? lightColor.length : i) - 1]!
        const ratio = (hour - pc.hour) / (lc.hour - pc.hour)
        color = new Color(pc.color).lerp(new Color(lc.color), ratio)
        directionalIntensity = pc.directional + (lc.directional - pc.directional) * ratio
        ambientIntensity = pc.ambient + (lc.ambient - pc.ambient) * ratio

        // 设置颜色与强度
        dl.color.set(color)
        dl.intensity = directionalIntensity
        al.color.set(color)
        al.intensity = ambientIntensity
        this.renderer.setClearColor(color)
        // 缓存到 remapCaching
        this.remapCaching.lightFogSky = '#' + color.getHexString()

        // 设置fog颜色
        if (this.scene.fog) this.scene.fog.color = color

        // 夜晚发光？
        // this.modelMaterials.glass.emissiveIntensity = Math.sin(2 * Math.PI * (ratio + 0.5)) * 3

        // 太阳与 y轴 (up)的极角，12点时为0，6点和18点为 PI/2，24点为 PI
        const phi = Math.PI * (Math.cos(Math.PI * 2 * (hour / 24)) + 1)
        // 太阳绕Y轴(up)的方位角，正北为0
        const theta = (-Math.PI * hour) / 12
        this.sunPosition.setFromSphericalCoords(this.options.sunDistance, phi < 0 ? 0 : phi, theta)
        dl.position.copy(this.sunPosition)
        dl.lookAt(new Vector3())
        return true
      }
      return false
    })
  }

  addAxis(length = 100) {
    this.ignored.add(new AxesHelper(length))
  }

  /** 添加指定大小的地面 */
  addGround(ground?: Mesh) {
    if (!ground) {
      const s = this.options.groundSize
      const geom = new PlaneGeometry(s, s).rotateX(-Math.PI / 2)
      ground = new Mesh(geom, presetOtherMaterials.ground)
      ground.renderOrder = -1
      ground.receiveShadow = true
    }

    this.ignored.add(ground)

    // 缓存到 remapCaching
    this.remapCaching.ground = '#' + presetOtherMaterials.ground.color.getHexString()
  }

  /** 添加雾气效果 */
  addFog() {
    const { near, far } = this.options.fog
    this.scene.fog = new Fog(this.remapCaching.lightFogSky, near, far)
  }

  /** 按绑定的DOM设置render和camera尺寸 */
  resizeScene() {
    const { clientHeight, clientWidth } = this.parent
    this.renderer.setSize(clientWidth, clientHeight)
    this.camera.aspect = clientWidth / clientHeight
    this.camera.updateProjectionMatrix()
  }

  /** 清理场景 */
  clear() {
    this.scene.children
      .filter((x) => !x.userData.ignored)
      .forEach((x) => {
        x.removeFromParent()
        disposeAll(x)
      })
  }

  /** 添加单个建筑 */
  add(raw: MagizTypes.DataJSON.ModelData, options?: Partial<MagizTypes.GenerateOptions>) {
    const modelGroup = generate(raw, options)
    this.scene.add(modelGroup)
    return modelGroup
  }

  /** 清理场景并生成模型，维护全局的颜色列表，返回总指标 */
  async refresh(
    raws: MagizTypes.DataJSON.ModelData[],
    options?: Partial<MagizTypes.GenerateOptions>,
    handleEachBuilding?: (g: Group) => void
  ): Promise<{ floorArea: number; maxFloors: number }> {
    this.clear()
    this.colors = {}

    const info = { floorArea: 0, maxFloors: 0 }
    let maxHeight = 0

    // 从解析数据生成建筑
    raws.forEach((raw) => {
      const modleGroup = this.add(raw, options)
      if (handleEachBuilding) handleEachBuilding(modleGroup)

      // 计算指标
      const h = raw.styleParams.height || 0
      const { floors, floorArea } = raw.info
      info.floorArea += floorArea * floors
      if (info.maxFloors < floors) info.maxFloors = floors
      if (maxHeight < h) maxHeight = h
    })

    return info
  }
}

function animate(v: View) {
  v.controls.update()
  v.renderer.render(v.scene, v.camera)
  for (const f in v.animations) {
    try {
      v.animations[f]!()
    } catch (error) {
      console.error(`animation "${f}" error !!!`, error)
    }
  }
  // 开发时的HMR导致多个渲染循环，须通过检查dom元素自动终止
  if (document.body.contains(v.renderer.domElement)) requestAnimationFrame(() => animate(v))
}

/** 添加镜头控制 */
function initOrbitControls(view: View, params?: Partial<typeof orbitControlsOptions>) {
  const options: typeof orbitControlsOptions = Object.assign(orbitControlsOptions, params)
  const ctrls = new OrbitControls(view.camera, view.renderer.domElement)

  // OrbitControls 可以 saveState() 然后 reset()
  ctrls.zoomToCursor = options.zoomToCursor
  ctrls.enableDamping = options.enableDamping
  ctrls.enablePan = options.enablePan
  ctrls.maxDistance = 10000

  const { autoRotate, moveNear } = options
  if (autoRotate > 0) {
    ctrls.autoRotateSpeed = autoRotate
    ctrls.autoRotate = true
  }
  if (moveNear > 0) {
    ctrls.addEventListener('change', () => {
      if (ctrls.getDistance() < moveNear) {
        const v = new Vector3()
        ctrls.object.getWorldDirection(v)
        ctrls.target.add(v.setLength(9))
      }
    })
  }

  return ctrls
}
